/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
import org.eclipse.imagen.media.util.CaselessStringArrayTable;
import org.eclipse.imagen.util.Range;

/**
 * A concrete implementation of the <code>ParameterListDescriptor</code> interface.
 *
 * @see ParameterListDescriptor
 * @since JAI 1.1
 */
public class ParameterListDescriptorImpl implements ParameterListDescriptor, java.io.Serializable {

    /** The number of parameters in the list described by this parameter. */
    private int numParams;

    /** The names of each parameter. */
    private String[] paramNames;

    /**
     * The <code>Class</code> type of each parameter. There is one-to-one mapping between this and <code>paramNames
     * </code>.
     */
    private Class[] paramClasses;

    /**
     * The default values for of each parameter. There is one-to-one mapping between this and <code>paramNames</code>.
     * If there is no default value for a given parameter, it is initialized with <code>
     * ParameterListDescriptor.NO_PARAMETER_DEFAULT</code>
     */
    private Object[] paramDefaults;

    /** Defines the valid parameter values for each parameter. */
    private Object[] validParamValues;

    /**
     * A <code>CaselessStringArrayTable</code> mapping the parameter names to their indices in the above arrays in a
     * case-insensitive manner.
     */
    private CaselessStringArrayTable paramIndices;

    /** The <code>Object</code> to reflect upon for enumerated parameters. */
    private Object descriptor;

    /** Indicates if the <code>validParamValues</code> field has been initialized. */
    private boolean validParamsInitialized = false;

    /**
     * Uses reflection to examine "descriptor" for <code>public</code>, <code>static</code> <code>final</code> <code>
     * Field</code>s that are instances of "paramClass".
     *
     * @param descriptor the object to be reflected upon.
     * @param paramClass the parameter class
     * @return a <code>Set</code> of enumerated values.
     * @throws IllegalArgumentException if descriptor is <code>null</code> or paramClass is <code>null</code>
     * @throws IllegalArgumentException if "paramClass" is not an instance of <code>EnumeratedParameter</code>
     */
    public static Set getEnumeratedValues(Object descriptor, Class paramClass) {

        if ((descriptor == null) || (paramClass == null))
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));

        // If not an enumerated parameter, return null
        if (!EnumeratedParameter.class.isAssignableFrom(paramClass))
            throw new IllegalArgumentException(
                    JaiI18N.formatMsg("ParameterListDescriptorImpl10", new Object[] {paramClass.getName()}));

        Field[] fields = descriptor.getClass().getDeclaredFields();

        if (fields == null) return null;

        // Look for all parameters which are instance of EnumeratedParameter.
        int numFields = fields.length;
        Set valueSet = null;

        // Look for all fields which are static, final
        // instances of the class of this parameter.
        for (int j = 0; j < numFields; j++) {
            Field field = fields[j];
            int modifiers = field.getModifiers();
            if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
                Object fieldValue = null;
                try {
                    fieldValue = field.get(null);
                } catch (Exception e) {
                    // Ignore exception
                }
                if (paramClass.isInstance(fieldValue)) {
                    if (valueSet == null) {
                        valueSet = new HashSet();
                    }

                    if (valueSet.contains(fieldValue)) {
                        // This error is a coding error
                        // which should be caught by the
                        // developer the first time the
                        // bogus descriptor is loaded.
                        throw new UnsupportedOperationException(JaiI18N.getString("ParameterListDescriptorImpl0"));
                    }
                    // Save parameter value in Set.
                    valueSet.add(fieldValue);
                }
            }
        }

        return valueSet;
    }

    /**
     * A wrapper method to get the valid parameter values for the specified parameter index. This makes sure that the
     * field has been initialized if it wasnt done before.
     */
    private Object getValidParamValue(int index) {

        if (validParamsInitialized) return validParamValues[index];

        synchronized (this) {
            if (validParamValues == null) {
                validParamValues = new Object[numParams];
            }

            Class enumeratedClass = EnumeratedParameter.class;

            for (int i = 0; i < numParams; i++) {
                if (validParamValues[i] != null) continue;

                if (enumeratedClass.isAssignableFrom(paramClasses[i])) {
                    validParamValues[i] = getEnumeratedValues(descriptor, paramClasses[i]);
                }
            }
        }

        validParamsInitialized = true;

        return validParamValues[index];
    }

    /** Constructor for descriptors that dont have any parameters. */
    public ParameterListDescriptorImpl() {
        this.numParams = 0;
        this.paramNames = null;
        this.paramClasses = null;
        this.paramDefaults = null;
        this.paramIndices = new CaselessStringArrayTable();
        this.validParamValues = null;
    }

    /**
     * Constructor.
     *
     * @param descriptor the object to be reflected upon for enumerated values
     * @param paramNames the names of each parameter. can be <code>null</code> if there are no parameters.
     * @param paramClasses the <code>Class</code> type of each parameter. can be <code>null</code> if there are no
     *     parameters.
     * @param paramDefaults the default values for each parameter. can be <code>null</code> if there are no parameters
     *     or if there are no default values, in which case the parameter defaults are assumed to be <code>
     * 	ParameterListDescriptor.NO_PARAMETER_DEFAULT</code>
     * @param validParamValues defines the valid values for each parameter.
     *     <p>Each element of this array can be <code>null</code> (if the parameter can take on any value of its class
     *     or if it is an enumerated parameter whose values are to be auto-detected - see <code>getEnumeratedValues
     *   </code>), or a <code>Set</code> (for user specified enumerated values) or a <code>Range</code> (for parameters
     *     that are <code>Comparable</code>.)
     *     <p>The valid set of values for an object which is neither an <code>EnumeratedParameter</code> nor <code>
     *     Comparable</code> should just be the set of all possible instances of the associated class, i.e., the
     *     parameter has to be an instance of the specified class.
     *     <p>If this array itself is <code>null</code> then it is treated as an array full of <code>null</code>s as
     *     described above.
     * @throws IllegalArgumentException if <code>paramNames</code> is non-null and the number of <code>paramClasses
     *     </code> or a non-null <code>paramDefaults</code> does not match the length of <code>paramNames</code>
     * @throws IllegalArgumentException if <code>null</code> is passed in for <code>validParamValues</code> for a
     *     parameter whose class is of <code>EnumeratedParameter</code> type.
     */
    public ParameterListDescriptorImpl(
            Object descriptor,
            String[] paramNames,
            Class[] paramClasses,
            Object[] paramDefaults,
            Object[] validParamValues) {

        int numParams = (paramNames == null) ? 0 : paramNames.length;

        if ((paramDefaults != null) && (paramDefaults.length != numParams))
            throw new IllegalArgumentException("paramDefaults" + JaiI18N.getString("ParameterListDescriptorImpl1"));

        if ((validParamValues != null) && (validParamValues.length != numParams))
            throw new IllegalArgumentException("validParamValues" + JaiI18N.getString("ParameterListDescriptorImpl2"));

        this.descriptor = descriptor;

        if (numParams == 0) {

            if ((paramClasses != null) && (paramClasses.length != 0))
                throw new IllegalArgumentException("paramClasses" + JaiI18N.getString("ParameterListDescriptorImpl3"));

            this.numParams = 0;
            this.paramNames = null;
            this.paramClasses = null;
            this.paramDefaults = null;
            this.paramIndices = new CaselessStringArrayTable();
            this.validParamValues = null;

        } else {

            if ((paramClasses == null) || (paramClasses.length != numParams))
                throw new IllegalArgumentException("paramClasses" + JaiI18N.getString("ParameterListDescriptorImpl3"));

            this.numParams = numParams;
            this.paramNames = paramNames;
            this.paramClasses = paramClasses;
            this.validParamValues = validParamValues;

            //
            // If the defaults are null, fill in NO_PARAMETER_DEFAULT
            // Else, make sure they belong to the right class.
            //
            if (paramDefaults == null) {
                this.paramDefaults = new Object[numParams];

                for (int i = 0; i < numParams; i++)
                    this.paramDefaults[i] = ParameterListDescriptor.NO_PARAMETER_DEFAULT;
            } else {

                this.paramDefaults = paramDefaults;

                for (int i = 0; i < numParams; i++) {
                    if ((paramDefaults[i] == null)
                            || (paramDefaults[i] == ParameterListDescriptor.NO_PARAMETER_DEFAULT)) continue;

                    if (!paramClasses[i].isInstance(paramDefaults[i])) {
                        throw new IllegalArgumentException(
                                JaiI18N.formatMsg("ParameterListDescriptorImpl4", new Object[] {
                                    paramDefaults[i].getClass().getName(), paramClasses[i].getName(), paramNames[i]
                                }));
                    }
                }
            }

            //
            // Make sure that validParamValues belongs to the right class.
            //
            if (validParamValues != null) {

                Class enumeratedClass = EnumeratedParameter.class;

                for (int i = 0; i < numParams; i++) {

                    if (validParamValues[i] == null) continue;

                    if (enumeratedClass.isAssignableFrom(paramClasses[i])) {

                        // If paramClass[i] is an enumerated parameter, then
                        // the validParamValues[i] has to be a Set
                        if (!(validParamValues[i] instanceof Set))
                            throw new IllegalArgumentException(
                                    JaiI18N.formatMsg("ParameterListDescriptorImpl5", new Object[] {paramNames[i]}));

                    } else if (validParamValues[i] instanceof Range) {

                        Range range = (Range) validParamValues[i];

                        // If the validParamValues[i] is a Range, then
                        // the Range's class must match with paramClass[i]
                        if (!paramClasses[i].isAssignableFrom(range.getElementClass()))
                            throw new IllegalArgumentException(
                                    JaiI18N.formatMsg("ParameterListDescriptorImpl6", new Object[] {
                                        range.getElementClass().getName(), paramClasses[i].getName(), paramNames[i]
                                    }));

                    } else {

                        // Otherwise, the validParamValues[i] has to be
                        // an instance of the paramClasses[i]
                        if (!paramClasses[i].isInstance(validParamValues[i]))
                            throw new IllegalArgumentException(
                                    JaiI18N.formatMsg("ParameterListDescriptorImpl7", new Object[] {
                                        validParamValues[i].getClass().getName(),
                                        paramClasses[i].getName(),
                                        paramNames[i]
                                    }));
                    }
                }
            }

            paramIndices = new CaselessStringArrayTable(paramNames);
        }
    }

    /** Returns the total number of parameters. */
    public int getNumParameters() {
        return numParams;
    }

    /**
     * Returns an array of <code>Class</code>es that describe the types of parameters. If there are no parameters, this
     * method returns <code>null</code>.
     */
    public Class[] getParamClasses() {
        return paramClasses;
    }

    /**
     * Returns an array of <code>String</code>s that are the names of the parameters associated with this descriptor. If
     * there are no parameters, this method returns <code>null</code>.
     */
    public String[] getParamNames() {
        return paramNames;
    }

    /**
     * Returns an array of <code>Object</code>s that define the default values of the parameters. Default values may be
     * <code>null</code>. The <code>NO_PARAMETER_DEFAULT</code> static <code>Object</code> indicates that a parameter
     * has no default value. If there are no parameters, this method returns <code>null</code>.
     */
    public Object[] getParamDefaults() {
        return paramDefaults;
    }

    /**
     * Returns the default value of a specified parameter. The default value may be <code>null</code>. If a parameter
     * has no default value, this method returns <code>NO_PARAMETER_DEFAULT</code>.
     *
     * @param parameterName The name of the parameter whose default value is queried.
     * @throws IllegalArgumentException if <code>parameterName</code> is null or if the parameter does not exist.
     */
    public Object getParamDefaultValue(String parameterName) {
        return paramDefaults[paramIndices.indexOf(parameterName)];
    }

    /**
     * Returns the <code>Range</code> that represents the range of valid values for the specified parameter. Returns
     * <code>null</code> if the parameter can take on any value or if the valid values are not representable as a Range.
     *
     * @param parameterName The name of the parameter whose valid range of values is to be determined.
     * @throws IllegalArgumentException if <code>parameterName</code> is null or if the parameter does not exist.
     */
    public Range getParamValueRange(String parameterName) {

        Object values = getValidParamValue(paramIndices.indexOf(parameterName));

        if ((values == null) || (values instanceof Range)) return (Range) values;

        return null;
    }

    /**
     * Return an array of the names of all parameters the type of which is <code>EnumeratedParameter</code>.
     *
     * @return The requested array of names or <code>null</code> if there are no parameters with <code>
     *     EnumeratedParameter</code> type.
     */
    public String[] getEnumeratedParameterNames() {

        Vector v = new Vector();

        for (int i = 0; i < numParams; i++) {
            if (EnumeratedParameter.class.isAssignableFrom(paramClasses[i])) v.add(paramNames[i]);
        }

        if (v.size() <= 0) return null;

        return (String[]) v.toArray(new String[0]);
    }

    /**
     * Return an array of <code>EnumeratedParameter</code> objects corresponding to the parameter with the specified
     * name.
     *
     * @param parameterName The name of the parameter for which the <code>EnumeratedParameter</code> array is to be
     *     returned.
     * @throws IllegalArgumentException if <code>parameterName</code> is null or if the parameter does not exist.
     * @throws UnsupportedOperationException if there are no enumerated parameters associated with the descriptor.
     * @throws IllegalArgumentException if <code>parameterName</code> is a parameter the class of which is not a
     *     subclass of <code>EnumeratedParameter</code>.
     * @return An array of <code>EnumeratedParameter</code> objects representing the range of values for the named
     *     parameter.
     */
    public EnumeratedParameter[] getEnumeratedParameterValues(String parameterName) {

        int i = paramIndices.indexOf(parameterName);

        if (!EnumeratedParameter.class.isAssignableFrom(paramClasses[i]))
            throw new IllegalArgumentException(parameterName + ":" + JaiI18N.getString("ParameterListDescriptorImpl8"));

        Set enumSet = (Set) getValidParamValue(i);

        if (enumSet == null) return null;

        return (EnumeratedParameter[]) enumSet.toArray(new EnumeratedParameter[0]);
    }

    /**
     * Checks to see if the specified parameter can take on the specified value.
     *
     * @param parameterName The name of the parameter for which the validity check is to be performed.
     * @throws IllegalArgumentException if <code>parameterName</code> is null or if the parameter does not exist.
     * @throws IllegalArgumentException if the class of the object "value" is not an instance of the class type of
     *     parameter pointed to by the parameterName
     * @return true, if it is valid to pass this value in for this parameter, false otherwise.
     */
    public boolean isParameterValueValid(String parameterName, Object value) {
        int index = paramIndices.indexOf(parameterName);

        if ((value == null) && (paramDefaults[index] == null)) {
            return true;
        }

        // Make sure the object belongs to the right class
        if ((value != null) && !paramClasses[index].isInstance(value)) {
            throw new IllegalArgumentException(JaiI18N.formatMsg(
                    "ParameterListDescriptorImpl9",
                    new Object[] {value.getClass().getName(), paramClasses[index].getName(), parameterName}));
        }

        Object validValues = getValidParamValue(index);

        // If validValues is null then any value is acceptable.
        if (validValues == null) return true;

        // If validValues is a Range, make sure "value" lies within it.
        if (validValues instanceof Range) return ((Range) validValues).contains((Comparable) value);

        // If validValues is a Set, then make sure that "value" is contained
        // in the Set.
        if (validValues instanceof Set) return ((Set) validValues).contains(value);

        // Otherwise the value must be the same as validValues
        return value == validValues;
    }
}
